// (c) Copyright 2013-2014 Stephen Gold <sgold@sonic.net>
// Distributed under the terms of the GNU General Public License

/*
 This file is part of the Bats Game.

 The Bats Game is free software: you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by the
 Free Software Foundation, either version 3 of the License, or (at your
 option) any later version.

 The Bats Game is distributed in the hope that it will be useful, but
 WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 for more details.

 You should have received a copy of the GNU General Public License
 along with the Bats Game.  If not, see <http://www.gnu.org/licenses/>.
 */
package bats.unit;

import bats.Assets;
import bats.Bats;
import bats.SpatialProperties;
import bats.control.BatsDelayControl;
import bats.control.DeleteControl;
import bats.control.PickedControl;
import bats.control.RevealControl;
import bats.control.physics.GrowControl;
import bats.map.MapNode;
import bats.scene.SceneNode;
import com.jme3.asset.AssetManager;
import com.jme3.bullet.collision.shapes.CollisionShape;
import com.jme3.bullet.control.RigidBodyControl;
import com.jme3.math.Quaternion;
import com.jme3.math.Vector3f;
import com.jme3.scene.Geometry;
import com.jme3.scene.Node;
import com.jme3.scene.SceneGraphVisitor;
import com.jme3.scene.Spatial;
import java.util.Collection;
import java.util.logging.Logger;
import jme3utilities.MySpatial;
import jme3utilities.NameGenerator;
import jme3utilities.debug.SkeletonDebugControl;
import jme3utilities.math.MyMath;
import jme3utilities.math.MyVector3f;
import jme3utilities.math.VectorXZ;

/**
 * Fields and methods common to all units, including bobs, otos, and sinbads.
 * <p>
 * A unit is a physical object with an animated skeleton and a viewpoint. Each
 * unit starts out unimpaired, which makes it pickable, targetable, and capable
 * of moving on its own initiative.
 * <p>
 * Units may become impaired. Impairment renders a unit unpickable,
 * untargetable, and incapable of moving on its own initiative.
 * <p>
 * Each unit has the following controls: Anim, Picked, Signal, Skeleton,
 * SkeletonDebug, Target, and Viewpoint. In simulation mode, it adds a Delete
 * control.
 *
 * @author Stephen Gold <sgold@sonic.net>
 */
public abstract class UnitClass {
    // *************************************************************************
    // constants

    /**
     * when a unit is being added, it will be automatically deleted if not
     * revealed within this time interval (in seconds)
     */
    final private static float revealTimeout = 1f;
    /**
     * message logger for this class
     */
    final private static Logger logger =
            Logger.getLogger(UnitClass.class.getName());
    // *************************************************************************
    // fields
    /**
     * mass of a standard-sized unit (in kilograms, &gt;0): invoke
     * setStandardMass() to override
     */
    protected float standardMass = 70f;
    /**
     * model scale factor which yields a standard-size unit (meters per model
     * unit, &gt;0): invoke setStandardScale() to override
     */
    protected float standardScale = 1f;
    /**
     * scene in which the units reside: set by constructor (not null)
     */
    final protected SceneNode scene;
    /**
     * name prefix for units of this class (not null)
     */
    final protected String namePrefix;
    // *************************************************************************
    // constructors

    /**
     * Instantiate for a specified scene and name prefix.
     *
     * @param scene where the units reside (not null)
     * @param namePrefix name prefix for this class of unit (not null)
     */
    protected UnitClass(SceneNode scene, String namePrefix) {
        assert scene != null;
        this.scene = scene;

        assert namePrefix != null;
        this.namePrefix = namePrefix;
    }
    // *************************************************************************
    // new methods exposed

    /**
     * Create a unit and add it to the scene, with its base at the specified map
     * location.
     *
     * @param modelName name of the model (not null)
     * @param relativeSize the unit's size relative to standard (&gt;0)
     * @param mapLocation initial map location of the unit's base (not null)
     * @param direction initial direction to face (unit vector)
     * @return a new spatial to represent the unit
     */
    Spatial add(float relativeSize, String modelName, VectorXZ mapLocation,
            VectorXZ direction) {
        assert relativeSize > 0f : relativeSize;
        assert modelName != null;
        assert mapLocation != null;
        assert direction != null;
        assert direction.isUnitVector() : direction;

        MapNode map = scene.getWorld().getMap();
        float baseY = map.getMapY(mapLocation);
        Vector3f baseLocation = mapLocation.toVector3f(baseY);
        Spatial result = add(relativeSize, modelName, baseLocation, direction);
        return result;
    }

    /**
     * Create a standard-sized unit and add it to the scene and the pick, with
     * its base at the specified world location.
     *
     * @param modelName name of the model (not null)
     * @param baseLocation initial world location of the unit's base
     * (unaffected, not null)
     * @param direction initial direction to face (unit vector)
     * @return a new Spatial which represents the unit
     */
    public Spatial add(String modelName, Vector3f baseLocation,
            VectorXZ direction) {
        assert modelName != null;
        assert baseLocation != null;
        assert direction != null;
        assert direction.isUnitVector() : direction;

        float standardSize = 1f;
        Spatial result = add(standardSize, modelName, baseLocation, direction);
        return result;
    }

    /**
     * Create a unit and add it to the scene and the pick, with its base at the
     * specified world location.
     *
     * @param relativeSize the unit's size relative to the standard (&gt;0)
     * @param modelName name of the model (not null)
     * @param baseLocation world coordinates of the unit's base (not null)
     * @param direction initial direction to face (unit vector)
     */
    abstract Node add(float relativeSize, String modelName,
            Vector3f baseLocation, VectorXZ direction);

    /**
     * Compute the mass of a unit.
     *
     * @param spatial which unit (not null)
     * @return mass (in kilograms, &gt;0)
     */
    public float calculateMass(Spatial spatial) {
        assert spatial != null;

        float relativeSize = getRelativeSize(spatial);
        float relativeVolume = MyMath.cube(relativeSize);
        float mass = standardMass * relativeVolume; // kilograms

        assert mass > 0f : mass;
        return mass;
    }

    /**
     * Delete all instances of this unit class in the scene.
     */
    void deleteAll() {
        SceneGraphVisitor visitor = new SceneGraphVisitor() {
            @Override
            public void visit(Spatial spatial) {
                if (isAnInstance(spatial)) {
                    scene.deleteSpatial(spatial);
                }
            }
        };
        scene.traverse(visitor);
    }

    /**
     * Get the world coordinates of a unit's pick indicator.
     *
     * @param spatial which unit to locate (not null)
     * @return a new vector (not null)
     */
    abstract public Vector3f getIndicatorLocation(Spatial spatial);

    /**
     * Calculate which direction a unit's pelvis is physically facing in map
     * coordinates.
     *
     * @param spatial which unit to orient (not null)
     * @return a new unit vector (not null)
     */
    public VectorXZ getMapPelvicDirection(Spatial spatial) {
        assert spatial != null;

        Vector3f worldDirection = getWorldPelvicDirection(spatial);
        VectorXZ mapDirection = MyVector3f.horizontalDirection(worldDirection);

        return mapDirection;
    }

    /**
     * Access a unit's move control.
     *
     * @param spatial which unit to access (not null)
     * @return the unit's move control or null if it has none
     */
    public MoveControl getMoveControl(Spatial spatial) {
        assert spatial != null;

        for (int index = 0; index < spatial.getNumControls(); index++) {
            Object object = spatial.getControl(index);
            if (object instanceof MoveControl) {
                return (MoveControl) object;
            }
        }
        return null;
    }

    /**
     * Compute the relative size of a specified unit.
     *
     * @param spatial which unit to measure (not null)
     * @return linear size relative to a standard unit of the same type (&gt;0)
     */
    public float getRelativeSize(Spatial spatial) {
        assert spatial != null;

        float worldScale = MySpatial.getUniformScale(spatial); // wu per mu
        float meterScale = worldScale * scene.getScaleFactor(); // meters per mu
        float size = meterScale / standardScale;

        assert size > 0f : size;
        return size;
    }

    /**
     * Get the world locations of a unit's targets.
     *
     * @param spatial which unit to locate (not null)
     * @return a new list (not null) of vectors (not null)
     */
    abstract public Collection<Vector3f> getTargets(Spatial spatial);

    /**
     * Get the world coordinates of a unit's viewpoint.
     *
     * @param spatial which unit to locate (not null)
     * @return a new vector
     */
    abstract public Vector3f getViewpointLocation(Spatial spatial);

    /**
     * Get the world orientation of a unit's viewpoint.
     *
     * @param spatial which unit to orient (not null)
     * @return a new instance
     */
    abstract public Quaternion getViewpointOrientation(Spatial spatial);

    /**
     * Impair a unit.
     *
     * @param spatial which unit to impair
     */
    abstract public void impair(Spatial spatial);

    /**
     * Test whether a spatial is a unit belonging to this UnitClass.
     *
     * @param spatial which unit (not null)
     * @return true if the unit belongs, otherwise false
     */
    public boolean isAnInstance(Spatial spatial) {
        String name = spatial.getName();
        boolean result = NameGenerator.isFrom(name, namePrefix);
        return result;
    }

    /**
     * Test whether the specified unit is impaired.
     *
     * @param spatial which unit (not null)
     * @return true if the unit is impaired; false if not impaired
     */
    abstract public boolean isImpaired(Spatial spatial);

    /**
     * Create a standard-sized unit with its base at the origin, facing south,
     * and add it to the scene.
     *
     * @param pickFlag if true, add the unit to the pick; if false the new unit
     * will be unpicked
     * @param modelName name of the model to load (not null)
     */
    public void load(boolean pickFlag, String modelName) {
        assert modelName != null;

        float standardSize = 1f;
        load(standardSize, pickFlag, modelName);
    }

    /**
     * Create a custom-sized unit with its base at the origin, facing south, and
     * add it to the scene.
     *
     * @param relativeSize the unit's size relative to the standard (&gt;0)
     * @param pickFlag if true, add the unit to the pick; if false the new unit
     * will be unpicked
     * @param modelName name of the model to load (not null)
     */
    public void load(float relativeSize, boolean pickFlag, String modelName) {
        assert relativeSize > 0f : relativeSize;
        assert modelName != null;

        Vector3f baseLocation = Vector3f.ZERO;
        VectorXZ facingSouth = new VectorXZ(-1f, 0f);
        Spatial spatial =
                add(relativeSize, modelName, baseLocation, facingSouth);
        SpatialProperties.setPicked(spatial, pickFlag);
    }

    /**
     * Reset a unit's viewpoint to its default position.
     *
     * @param spatial which unit to adjust (not null)
     */
    public void resetViewpoint(Spatial spatial) {
        assert spatial != null;

        SpatialProperties.setTiltAngle(spatial, 0f);
        SpatialProperties.setTurnAngle(spatial, 0f);
    }

    /**
     * Tilt a unit's viewpoint forward or back by a specified amount.
     * <p>
     * Does nothing. Meant to be overridden.
     *
     * @param spatial which unit to adjust (not null)
     * @param angle positive to tilt forward, negative to tilt back
     */
    public void tiltViewpointForward(Spatial spatial, float angle) {
    }

    /**
     * Turn a unit's viewpoint left or right by a specified amount.
     * <p>
     * Does nothing. Meant to be overridden.
     *
     * @param spatial which unit to adjust (not null)
     * @param angle positive to turn left, negative to turn right
     */
    public void turnViewpointLeft(Spatial spatial, float angle) {
    }

    /**
     * Update a unit's collision shape.
     *
     * @param spatial which unit to update (not null)
     */
    abstract public void updateCollisionShape(Spatial spatial);
    // *************************************************************************
    // new protected methods

    /**
     * Add the standard abstract controls to a unit.
     *
     * @param geometry (not null) TODO should be a list
     * @param mainControl (not null)
     * @param spatial which unit to control (not null)
     */
    protected void addAbstractControls(Geometry geometry,
            RigidBodyControl mainControl, Spatial spatial) {
        assert geometry != null;
        /*
         * Add indicator controls.
         */
        PickedControl pickedControl = new PickedControl();
        spatial.addControl(pickedControl);
        TargetControl targetControl = new TargetControl();
        spatial.addControl(targetControl);
        ViewpointControl viewpointControl = new ViewpointControl();
        spatial.addControl(viewpointControl);
        /*
         * Add skeleton debug control.
         */
        AssetManager assetManager = Assets.getManager();
        SkeletonDebugControl skeletonDebugConrol =
                new SkeletonDebugControl(assetManager);
        spatial.addControl(skeletonDebugConrol);

        if (Bats.isSimulationMode()) {
            /*
             * Add simulation-mode controls.
             */
            DeleteControl deleteControl = new DeleteControl(scene);
            spatial.addControl(deleteControl);

            BatsDelayControl delayControl = new BatsDelayControl();
            spatial.addControl(delayControl);
            delayControl.addSlave(deleteControl);
            delayControl.setRemainingSeconds(revealTimeout);
            delayControl.setEnabled(true);

            RevealControl revealControl = new RevealControl(geometry);
            spatial.addControl(revealControl);

            CollisionShape shape = mainControl.getCollisionShape();
            GrowControl growControl = new GrowControl(shape, scene);
            spatial.addControl(growControl);
            growControl.addSuccessor(pickedControl);
            growControl.addSuccessor(revealControl);
            growControl.addSuccessor(mainControl);
            growControl.addTimeout(delayControl);
        } else {
            ((Node) spatial).attachChild(geometry);
            mainControl.setEnabled(true);
            pickedControl.setEnabled(true);
            scene.getWorld().getPickables().add(spatial);
        }
    }

    /**
     * Get the model direction which a unit's pelvis faces.
     *
     * @param spatial which unit to orient (not null)
     * @return a new unit vector
     */
    abstract protected Vector3f getModelPelvicDirection(Spatial spatial);

    /**
     * Calculate which direction a unit's pelvis is physically facing in world
     * coordinates.
     * <p>
     * This is also the direction to which it will launch missiles.
     *
     * @param spatial which unit (not null)
     * @return a new unit vector
     */
    protected Vector3f getWorldPelvicDirection(Spatial spatial) {
        assert spatial != null;

        Quaternion orientation = MySpatial.getWorldOrientation(spatial);
        Vector3f localPelvis = getModelPelvicDirection(spatial);
        Vector3f direction = orientation.mult(localPelvis);
        return direction;
    }

    /**
     * Alter the mass of a standard-sized unit. This won't affect any units
     * which have already been created.
     *
     * @param newMass (in kilograms, &gt;0)
     */
    protected void setStandardMass(float newMass) {
        assert newMass > 0f : newMass;
        standardMass = newMass;
    }

    /**
     * Alter the model scale factor which yields a standard-size unit. This
     * won't affect any units which have already been created.
     *
     * @param newScaleFactor (meters per model unit, &gt;0)
     */
    protected void setStandardScale(float newScaleFactor) {
        assert newScaleFactor > 0f : newScaleFactor;
        standardScale = newScaleFactor;
    }

    /**
     * Generate a unique name for a new unit.
     *
     * @return a unique string (not null)
     */
    protected String uniqueName() {
        String name = scene.getNameGenerator().unique(namePrefix);
        return name;
    }
}